Adds multi-select support#1360
Conversation
| let imageHistoryBrowser = new GenPageBrowserClass('image_history', listOutputHistoryFolderAndFiles, 'imagehistorybrowser', 'Thumbnails', describeOutputFile, selectOutputInHistory, | ||
| `<label for="image_history_sort_by">Sort:</label> <select id="image_history_sort_by"><option>Name</option><option>Date</option></select> <input type="checkbox" id="image_history_sort_reverse"> <label for="image_history_sort_reverse">Reverse</label>   <input type="checkbox" id="image_history_allow_anims" checked autocomplete="off"> <label for="image_history_allow_anims">Allow Animation</label>`); | ||
| imageHistoryBrowser.enableBrowserMultiSelect = true; | ||
| imageHistoryBrowser.keepBrowserMultiSelectKeyAfterPrune = function(key, namesInCurrentList) { |
There was a problem hiding this comment.
match existing patterns for function declarations
|
|
||
| function clickImageInBatch(div) { | ||
| let imgElem = div.getElementsByTagName('img')[0]; | ||
| let multiSelectKey = getImageFullSrc(div.dataset.src); |
There was a problem hiding this comment.
there shouldn't be any multiselect handling outside of the browser class like this
| } | ||
| let isStarred = this.isStarred(model.data.name); | ||
| let starButton = { label: isStarred ? 'Unstar' : 'Star', onclick: () => { this.toggleStar(model.data.name); } }; | ||
| let starButton = { label: isStarred ? 'Unstar' : 'Star', onclick: () => { this.toggleStar(model.data.name); }, can_multi: true }; |
There was a problem hiding this comment.
Are you objecting to the , can_multi: true flag itself (how I've defined it, for example), or objecting to Star/Unstar action being available for multi-select action?
There was a problem hiding this comment.
you editing the way star is handled for multi, which is already covered properly for images, and presets/models should handle it the same as images
There was a problem hiding this comment.
Probably the best move is limit the PR to browser itself & images, preset/models can be separate
aka the universal rule of PR contributing: minimize the scope of edits per individual PR
| toggleStar(fullsrc, src); | ||
| } | ||
| }, | ||
| can_multi: true |
| if (div) { | ||
| removeImageBlockFromBatch(div); | ||
| } | ||
| if (imageHistoryBrowser.enableBrowserMultiSelect) { |
There was a problem hiding this comment.
this is questionably placed aaand also seems to be a mixup between the 'enable' and 'active' bools?
| return true; | ||
| } | ||
| let currentImageBatchDiv = getRequiredElementById('current_image_batch'); | ||
| for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { |
| { label: 'Direct Apply', onclick: () => applyOnePreset(preset.data) }, | ||
| { label: preset.data.is_starred ? 'Unstar' : 'Star', onclick: () => togglePresetStar(preset) }, | ||
| { label: 'Direct Apply', onclick: () => applyOnePreset(preset.data), can_multi: true }, | ||
| { label: preset.data.is_starred ? 'Unstar' : 'Star', onclick: () => togglePresetStar(preset), can_multi: true }, |
| { label: 'Edit Preset', onclick: () => editPreset(preset.data) }, | ||
| { label: 'Duplicate Preset', onclick: () => duplicatePreset(preset.data) }, | ||
| { label: 'Export Preset', onclick: () => exportOnePresetButton(preset.data) }, | ||
| { label: 'Export Preset', onclick: () => exportOnePresetButton(preset.data), can_multi: true }, |
There was a problem hiding this comment.
this can't function natively and needs separate impl
| this.runAfterUpdate = []; | ||
| this.refreshHandler = (callback) => callback(); | ||
| this.checkIsSmall(); | ||
| this.enableBrowserMultiSelect = false; |
There was a problem hiding this comment.
should probably be allowMultiSelect
| this.checkIsSmall(); | ||
| this.enableBrowserMultiSelect = false; | ||
| this.multiSelectActive = false; | ||
| this.multiSelectedKeys = new Set(); |
There was a problem hiding this comment.
this is liable to lead to state tracking inconsistencies, as jank as it feels putting class or .dataset's on the block divs is probably the stablest way to track
| textBlock.tabIndex = 0; | ||
| textBlock.innerHTML = desc.description; | ||
| div.appendChild(textBlock); | ||
| div.addEventListener('click', (e) => { |
There was a problem hiding this comment.
don't need a separate click handler, the select handler should cover it
| let getMeta = (metadata) => metadata ? (JSON.parse(metadata) || {}) : {}; | ||
| let metaParsed = getMeta(metadata); | ||
| let isStarred = (e) => { | ||
| let currentMeta = e && e.dataset ? getMeta(e.dataset.metadata) : {}; | ||
| if (Object.keys(currentMeta).length == 0) { | ||
| currentMeta = metaParsed; | ||
| } | ||
| return currentMeta.is_starred; | ||
| }; |
There was a problem hiding this comment.
There was a bug in your original implementation:
- metadata would initially not exist on an item, causing exception on
let metaParsed = JSON.parse(metadata); - metadata would be stale on reads below. Clicking "Enable Starred" would add the needed metadata and also star the items, but then clicking "Disable(d) Starred" would still see stale metadata and not unstar items
- Fixed speling
| let textBlock = createDiv(null, 'model-descblock'); | ||
| textBlock.tabIndex = 0; | ||
| textBlock.innerHTML = desc.description; | ||
| textBlock.addEventListener('click', (e) => { |
There was a problem hiding this comment.
What's with all these new click listeners?
There was a problem hiding this comment.
This one is necessary. I couldn't need get multi-select click to select a card if I clicked the description otherwise.
| } | ||
| let items = []; | ||
| for (let child of this.contentDiv.children) { | ||
| if (child.dataset && child.dataset.name && child.classList.contains('browser-multiselect-item-selected')) { |
There was a problem hiding this comment.
querySelectorAll should do this quick n simple
| } | ||
|
|
||
| /** | ||
| * Labels for bulk actions shared by every selected item, respecting `can_multi` / `multi_only`. |
There was a problem hiding this comment.
This feels like AI slop. Misinterpreted keywords and then proudly documented respect for the misunderstood keyword.
There was a problem hiding this comment.
To be fair, this PR has gone through several major revisions after feedback. I try to keep comments up to date.
It does point to my initial PR diffs probably being overly large if I sometimes mislabel a function, which is something I'm actively working on for the SwarmUI project.
| if (files.length == 0) { | ||
| return []; | ||
| } | ||
| let eligiblePerFile = []; |
There was a problem hiding this comment.
actually wtf is this whole function? Why was this built like this at all??? wat.
There was a problem hiding this comment.
I'm reducing the diff here, but this function actually does something important - it gates specific multi-select actions on filetypes the user selects.
A user can select any number of image, video, and audio files in the history browser and select an action like Star, or Delete.
However, it makes less sense for a user to select an image + audio file and allow them to do a media comparison.
The current implementation just iterates through each selected file and removes actions that cannot be applied to the entire set. Slightly wordy, I can reduce the diff without getting too clever, but I would push back strongly on this being removed completely.
There was a problem hiding this comment.
"image + audio" comparison is stupid and I would instead say an image + video comparison wouldn't make much sense. However, it would make more sense than image+audio.
I can maybe see a scenario where a user wants to popup an image and a video into a comparison modal to eyeball a bit? Not much of a scenario but if you want this code removed now I'm open to it.
|
Issues from testing:
|
| }); | ||
| } | ||
| else { | ||
| if (this.preservedMultiSelect.size == 0) { |
There was a problem hiding this comment.
This set is used and cleared directly within one function, so presumably can safely be function-local rather than on the instance ctor?
There was a problem hiding this comment.
kind of. Doing a filter or depth change doesn't need an object class instance variable, but the browser refresh button nukes everything. I couldn't get state to stick otherwise. I'm pushing one small change cleaning up tiny odds and ends but unless you have a suggested alternative method, this is the way I could get it to work.
Note that if a user clicks the Refresh button twice very quickly, state is still lost. I was going to add a timeout but then PR starts touching things unrelated to core feature.
| let desc = this.describe(file); | ||
| let labels = new Set(); | ||
| for (let button of desc.buttons) { | ||
| if (button.can_multi && (button.max_selected == null || files.length <= button.max_selected)) { |
There was a problem hiding this comment.
TBH I'm not a fan of the change you made here. The previous split if blocks were wordy and we could have merged a few of them, but this if now feels overloaded.
… enough for a Set to be worth it
CleanShot.2026-04-29.at.23.31.21.mp4